package cmd

import (
	"container/heap"
	"context"
	"fmt"
	"time"

	"code.alibaba-inc.com/agit/quarantine-helper/env"

	"code.alibaba-inc.com/agit/quarantine-helper/config"
	commitqueue "code.alibaba-inc.com/agit/quarantine-helper/queue"
	"code.alibaba-inc.com/agit/quarantine-helper/utils"
	"github.com/spf13/cobra"
)

func filterCommitsByRepository(oids []string) (map[string]bool, error) {
	newCommitMap := make(map[string]bool)
	oldCommits, err := utils.FilterObjects(oids, "commit", env.GetEnvsNoQuarantine())
	if err != nil {
		return newCommitMap, err
	}
	for _, oid := range oldCommits {
		newCommitMap[oid] = false
	}
	for _, oid := range oids {
		if _, ok := newCommitMap[oid]; !ok {
			newCommitMap[oid] = true
		}
	}
	return newCommitMap, nil
}

// TODO: (Jiuyang) This is a bad implementation and has to be refactored
func filterCommitsByBranchTip(commitMap map[string]bool, tip string) ([]string, error) {
	var (
		tipCommit *utils.GitCommit
		err       error
		ids       []string
	)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	count := 0
	commitHeap := make(commitqueue.PriorityQueue, 1)
	if tip != "" {
		tipCommit, err = utils.GetCommitByID(tip, env.GetEnvsWithQuarantine())
		if err != nil {
			tipCommit = nil
		}
	}

	/*
	 * We may want to update multiple branches in one push, with
	 * the following commands:
	 *
	 *     old-oid new-oid refs/heads/master
	 *     old-oid new-oid refs/heads/my/topic/1
	 *     old-oid new-oid refs/heads/my/topic/2
	 *
	 * New commits sent to the repository may be used for updating
	 * the above three references. But we may only want to check
	 * commit for the specific reference, such as "master", and
	 * want to ignore new commits for other branches.
	 */
	item := commitqueue.CreateCommitStruct(tipCommit)
	commitHeap[0] = &item
	heap.Init(&commitHeap)

	for count < config.GetLimit() && commitHeap.Len() > 0 {
		currentCommit := heap.Pop(&commitHeap).(*commitqueue.Commit)
		if currentCommit == nil || currentCommit.GetCommit() == nil {
			continue
		}
		commitID := currentCommit.GetCommit().Id
		toCheck, ok := commitMap[commitID]

		/*
		 * 1) the tip commit we uploaded not in commitMap? (insane!),
		 *    so it must be case 2 as follows:
		 * 2) it is one ancestor commit which not in commitMap, so ignore it.
		 */
		if !ok {
			continue
		}

		/* Ignore the commit we sent which is not a new commit */
		if !toCheck {
			continue
		}

		/*
		 * The commit we are interested in will be saved and we will
		 * mark it as ignored.
		 */
		ids = append(ids, commitID)
		commitMap[commitID] = false
		count++

		/*
		 * Continue to check parent commits we have interested in.
		 */
		for i := 0; i < len(currentCommit.GetCommit().ParentIds); i++ {
			parents, err := utils.GetLogData(ctx, currentCommit.GetCommit().Id, "%P")
			if err != nil {
				return nil, err
			}

			for _, parent := range parents {
				if len(parents) > 0 {
					p, err := utils.GetCommit(ctx, parent, env.GetEnvsWithQuarantine())
					if err != nil {
						return nil, err
					}
					item := commitqueue.CreateCommitStruct(p)
					heap.Push(&commitHeap, &item)
				}
			}
		}
	}

	return ids, nil
}

var filterCmd = &cobra.Command{
	Use:   "filter",
	Short: "filter out commits that are already in git repository.",
	Long: `This command's input is scan's output, it checks whether commits pushed to Quarantine are already in the repository
	and output commit ids of those not in the repository by default. If -b is set, it outputs only those commits that belongs to
	a specific branch`,
	RunE: func(cmd *cobra.Command, args []string) error {
		oids, err := utils.GetCommitIDArray(nil)
		if err != nil {
			return err
		}
		_, err = filter(oids)
		return err
	},
}

func filter(commitIDs []string) ([]string, error) {
	var (
		commitMap = make(map[string]bool)
		err       error
		oids      []string
	)
	if config.IsCommitFromWeb() {
		commitIDs = []string{config.GetBranchTip()}
		if !config.GetDisableStdout() {
			fmt.Println(commitIDs[0])
		}
		return commitIDs, nil
	}

	commitMap, err = filterCommitsByRepository(commitIDs)
	if err != nil {
		return nil, err
	}

	/*
	 * If no tip commit, return all new commits for this repo.
	 */
	if config.GetBranchTip() == "" {
		for oid, isNew := range commitMap {
			if !isNew {
				continue
			}
			oids = append(oids, oid)
		}
	} else {
		oids, err = filterCommitsByBranchTip(commitMap, config.GetBranchTip())
		if err != nil {
			return nil, err
		}
	}
	if !config.GetDisableStdout() {
		for _, oid := range oids {
			fmt.Println(oid)
		}
	}
	return oids, nil
}

func init() {
	rootCmd.AddCommand(filterCmd)
	filterCmd.Flags().StringVarP(&config.BranchTip, "branchTip", "b", "", "Only print commits that are ancestors of this commit.")
	filterCmd.Flags().IntVarP(&config.Limit, "limit", "l", 100, "The maximum commits count.")
}
